/*
 * 本程序存储在软盘0的第6个扇区开始，会同其他内核文件一起链接
 * 由 boot.s 加载到 0x10000 的位置，由 setup.s 移动到 0x0000 处
 * setup.s 进入保护模式后，通过一个长跳转到本程序开始执行
 * 本程序所做的工作内容如下：
 *      1. 重新设置 idt 和 gdt，并更新段寄存器
 *      2. 检测A20地址线是否真正开启
 *      3. 检测是否存在协处理器
 *      4. 设置分页，并把前16M物理地址和虚拟地址进行一一映射
 *      5. 利用ret返回到main函数执行（终于跳出汇编了）

    as --gstabs --32 -o head.o head.s
    ld -m elf_i386 -e startup_32 -Ttext 0x0000 head.o -g -o head.elf
    objcopy -O binary head.elf head.bin

    dd if=head.bin of=a.img bs=512 seek=5 conv=notrunc
 */

.equ screen_sel, 0x18                           # 屏幕显示内存 段选择符

.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area, _start, _print_str
_pg_dir:                            # 这里会存放页表，因此物理地址0开头的代码会最终被覆盖掉
_start:
    movl $0x10, %eax                # 数据段等寄存器选择符为 GDT 中的 0x10索引，在 setup.s 中临时设置了 GDT
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs
    lss _stack_start, %esp          # _stack_start 在 sched.c 中定义，%esp指向一个页面的末端，%ss为0x10;暂时先定义一个

    /* 重新设置 idt 和 gdt */
    call setup_idt
    call setup_gdt

    /* 设置完idt和gdt之后需要重新加载一下段寄存器 */
	movl $0x10,%eax
	mov %ax,%ds                     # %cs 寄存器已经在 setup_gdt 中重新设置
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	lss _stack_start,%esp

    /*
	 	%eax = 0 
		将地址0的值赋值为1，并检查地址0的值和地址1M的值是否相等
		如果A20开启，那不太可能一直相等
	 */
    xorl %eax, %eax
1:  incl %eax
    movl %eax, 0x000000
    cmpl %eax, 0x100000
    je 1b


    /*
		接下来设置 cr0 寄存器的相关标志位
		注意：486应该置位bit16，cr0的bit16是WP，也就是写保护标志
		当该标志位置为1时，会禁止超级用户程序（例如特权级为0的程序）向用户级只读页面执行写操作
		有利于在创建进程时实现写时复制（Copy on Write）操作
	 */
    movl %cr0, %eax
    andl $0x80000011, %eax      # 实际上此处 PG 位为0，主要是把这三个bit提取出来
    orl $2, %eax                # MP：监测协处理器。若MP=1，当TS=1时WAIT指令会产生设备不可用异常；MP=0时，WAIT指令会忽略TS标志的值。
    movl %eax, %cr0
    call check_x87              # 检查协处理器是否存在
    jmp after_page_tables       # 准备跳转进入 main 函数，模拟了一个函数调用方式，使用ret，堆栈上线准备好相关参数

check_x87:
	fninit					# 向协处理器发出初始化命令
	fstsw %ax				# 获取协处理器的状态
	cmpb $0,%al				# 初始化后的状态应该为0，不为0表示协处理器不存在
	je 1f
	movl %cr0,%eax
	xorl $6,%eax            # 这里是异或操作，因为之前 MP 位已经设置为1，EM已经设置为0，异或会让 MP 为0，EM为1
	movl %eax,%cr0			# 不应在协处理器，则需要设置EM位（仿真）
	ret
.p2align 2
	/* 把协处理器设置为保护模式，387会忽略 */
1:	.byte 0xDB,0xE4
	ret

/*
	设置中断描述符表，256个中断描述符都设置成一样的了，等待后续会根据实际情况进一步设置
	每个中断描述符是8字节，从高到低分别是
	ignore_int 地址的高16位 0x8E00 0x0008 ignore_int地址的第16位

	其含义分别为:
	描述符对应的程序入口地址就是 ignore_int
	对应的段选择符是 0x0008 (也就是 GDT 表中的 第二项 代码段)
	0x8E00 = 0b 1000 1110 0000 0000
			P存在位为1，DPL=0为内核态特权级，01110表示中断门描述符（进入中断处理程序时会自动屏蔽中断，防止中断嵌套）
 */
setup_idt:
    lea ignore_int, %edx
    movl $0x00080000, %eax
    movw %dx, %ax
    movw $0x8E00, %dx

    lea _idt, %edi
    mov $256, %ecx
rp_sidt:
    movl %eax, (%edi)
    movl %edx, 4(%edi)
    addl $8, %edi
    dec %ecx
    jne rp_sidt
    lidt idt_descr                      # 重新加载中断描述符表寄存器
    ret

setup_gdt:
	lgdt gdt_descr				# 重新加载全局描述符表寄存器
	ret

.org 0x1000                     # 4K 位置，前面是页目录表（1个页目录表，共1024个页表，但是此处只定义了4个页表，占用 4 * 4M = 16M）
pg0:

.org 0x2000				# 8K处
pg1:

.org 0x3000				# 12K处
pg2:

.org 0x4000				# 16K处
pg3:

.org 0x5000				# 20K处

/*
	DMA无法访问缓存块时候，下面这个区域给软盘使用
 */
_tmp_floppy_area:
	.fill 1024,1,0

after_page_tables:
    /*
		先准备好堆栈的调用参数，然后 jmp 跳入到 setup_paging 进行分页并开启分页模式
		最后在 setup_paging 使用 ret 模拟函数返回，这个时候这里的堆栈中参数就有用了，会被当作调用返回
		堆栈参数如下

	 */
    pushl $0
    pushl $0
    pushl $0
    pushl $L6                       # main 如果再返回，返回地址就是它

	/* 先初始化当前的行号和列号，当前行号的下一行开始main中的显示 */
	xor %eax, %eax
	movb (0x90001), %al
	incl %eax
	imull $80, %eax, %eax
	movl %eax, scr_loc
	
    pushl $main                    # 返回地址：下一个指令的位置，实际就是会跳入到 main() 中执行
    jmp setup_paging
L6: jmp L6

int_msg:
	.asciz "Unknown interrupt\n\r"
.p2align 2
ignore_int:
	pushl %eax
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	pushl $int_msg
	#call _printk                   # 还未实现，先不放开
	popl %eax
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret

.p2align 2
setup_paging:
	# 总数量是5K：1个页目录+4个页表
	movl $1024*5,%ecx
	xorl %eax,%eax
	xorl %edi,%edi			# 页目录起始物理地址是 0x000
	cld;rep;stosl			# 将 %eax 存储到 %es:%edi 指向的位置，实际就是把页表所占用的 5K 内存初始化为 0

    /*
	 	_pg_dir指向的地址是0，下面是填充了页目录的前面4项，也就是填充了前4个页表，共16M的地址映射
		$pg0+7 表示 页表0的地址 + 0x07，由于 $pg0 已经是 4K对齐，因此其低12位已经为0，刚好对应页表项的高20位，页表
			最低三位为1，分别表示U/S=1 任何特权级都可以访问，R/W=1 页面可读可写，P=1 页目录有效
			U/S为1而不是0的原因是，这段物理内存会被用户态任务0和任务1共同使用，
			当然任务0和任务1是只能访问他们自身的代码和数据，通过段保护做到这一点
			另外一个点是：1M以下的物理内存可以共享读，但是没有实现COW，一旦非内核来改写，会panic
	 */
	movl $pg0+7, _pg_dir
	movl $pg1+7, _pg_dir+4
	movl $pg2+7, _pg_dir+8
	movl $pg3+7, _pg_dir+12

    /*
		现在开始填充各个页表了，这里是从页表的最高位置，也就是页表3的末端 （起始地址 + 4K）开始填充
		填充的值是：16Mb - 4096 + 7 (r/w user,p)，其中 16Mb - 4096 是 最后一个页的起始地址，填充到页表的最后一项
		然后往前填充页表，每次地址减掉 4K
		这里填充为用户也有权限可读可写了，实际通过虚拟地址空间的保护模式，和具体page的权限，来限制最终的访问权限
	 */
	movl $pg3+4092,%edi
	movl $0xfff007,%eax
	std						# 重要，设置填充方向是从高往低
1:	stosl
	subl $0x1000,%eax
	jge 1b

    /*
		好了，现在页目录和页表都已经是设置完毕
		来开启分页：
		1. 先把页表寄存器加载（cr3寄存器），让cr3指向页目录的起始物理地址，实际就是地址0处
		2. 开启分页，就是把PG位（cr0寄存器的最高位）设置为1
	 */
	xorl %eax,%eax
	movl %eax,%cr3              # cr3 - 页目录起始物理地址
	movl %cr0,%eax
	orl $0x80000000,%eax
	movl %eax,%cr0              # 设置 PG 位
	ret                         # 利用ret刷新cpu指令缓存

.p2align 2				# 4字节对齐
.word 0
idt_descr:
	.word 256*8-1		# idt 表共 256 项
	.long _idt
.p2align 2				# 4字节对齐
.word 0
gdt_descr:
	.word 256*8-1		# 段限长要减1
	.long _gdt		    # _gdt 作为基地址

.p2align 3			    # 8字节对齐
_idt:	.fill 256,8,0	# idt 未初始化，先都填充为默认中断处理函数，然后根据需要进行设置，此时还没有开中断

/*
	16M 的代码段和数据段，基地址是0，DPL=0
	G=1，D/B=1表示32位
    用了4个，剩余252个可以用于任务的 LDT 和 TSS
 */
_gdt:
    .quad 0x0000000000000000	/* 空描述符，保留不用 */
	.quad 0x00c09a0000000fff	/* 16Mb */
	.quad 0x00c0920000000fff	/* 16Mb */
    .quad 0x00c0920b80000002    # 显示段，选择符为 0x18，偏移地址为 0xb8000
	#.quad 0x0000000000000000	/* 保留未用 */
	.fill 252,8,0			    /* 预留给任务的 LDT 和 TSS 用 */


/*
    由于还没有实现 C 语言的部分，先临时给一个堆栈用
    等后续实现了，再去掉
 */
.p2align 4
    .fill 4096,1,0              # 4K的堆栈
_stack_start:
    .long _stack_start                        # 堆栈段偏移位置
    .word 0x10                                # 堆栈段选择符，实际就是内核数据段
/*
 * 显示字符子程序
 *    取当前光标的位置，并把%al中的字符显示到屏幕
 *    整个屏幕可以显示 80 * 25 个字符
 */
scr_loc: .long 0                                # 字符显示位置
_write_char:
   push %gs                                     # 保存需要用到的寄存器，eax 由调用方保存
   pushl %ebx
   mov $screen_sel, %ebx                        # 让 gs 寄存器指向显示内存段 （0xb8000）
   mov %bx, %gs
   mov scr_loc, %bx                             # 获取当前字符显示的位置
   shl $1, %ebx                                 # 每个字符显示要占用2个字节，一个字节会用于显示属性的控制，因此实际的位置 * 2
   movb %al, %gs:(%ebx)                         # 将待显示的字符放入正确的位置

   # 计算下一个待显示字符的位置，具体方法位：当前字符的位置 / 2 + 1
   # 如果超过 2000 （80 * 25），则恢复到开头
   shr $1, %ebx
   incl %ebx
   cmpl $2000, %ebx
   jb 1f
   mov $0, %ebx
1: mov %ebx, scr_loc                            # 保存下一个显示位置
   popl %ebx
   pop %gs
   ret

_print_str:
	/*
	 * 打印字符串，其中 %ecx 存储字节数，%ebx 存储字符串地址
	 */
	pushl %ebx
	pushl %ecx
rp_write:
	cmpl $0, %ecx
	je done_wirte
	movl (%ebx), %eax
	call _write_char
	subl $1, %ecx
	addl $1, %ebx
	jmp rp_write
done_wirte:
	popl %ecx
	popl %ebx
	ret

